/*
 * ============================================================================
 *
 *  Rotoblin
 *
 *  File:			rotoblin.survivorexploitfixes.sp
 *  Type:			Module
 *  Description:	Fixes some exploits for the suvivor ledge grabs
 *
 *  Copyright (C) 2011 pwn <ajparise@gmail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * ============================================================================
 */

// --------------------
//       Private
// --------------------

enum exploitState
{
	PLAYER_PULLED,						// 1 or 0 indicating if the player is pulled
	PLAYER_PULLED_BY,					// User ID of smoker who caused the pull
	PLAYER_HANGING,						// 1 or 0 indicating if the player is hanging
	PLAYER_WAS_INCAPPED,				// 1 or 0 indicating if the player was incapped by the pulled ledge hang
	HEALTH_SOLID,						// Solid health of the player prior to the ledge (only if they were pulled)
	HEALTH_HANGING_OR_RELEASE,			// Incap health of the player when they first went off the ledge OR after tongue has been release
	HEALTH_REVIVE_START,				// Incap health of the player on revival
	HEALTH_SOLID_REVIVE,				// Calculated solid health the player should have when revived
	Float:HEALTH_TEMP_REVIVE,			// Calculated temp health the player should have when revive
	HANDLE_REVIVE_HEALTH,				// 1 or 0 indicating if we should handle solid health on revive
	HANDLE_REVIVE_TEMP_HEALTH,			// 1 or 0 indicating if we should handle temporary health on revive
	Float:HEALTH_BUFFER,				// Health buffer value when the player grabbed the ledge
	Float:HEALTH_BUFFER_TIME,			// Health buffer time value when the player grabbed the ledge
	Handle:TIMER_HEALTH_TRACKER,		// Handle of pulled and hanging timer (calculates health)
	Handle:TIMER_HEALTH_UPDATER			// Handle of health update timer (restores health and bonus)
}

static 			playerState[MAXPLAYERS+1][exploitState];
static			Float:SMOKER_CLAW_DAMAGE				= 3.0;
static					g_iDebugChannel					= 0;
static	const	String:	DEBUG_CHANNEL_NAME[]			= "SurvivorExploitFixes";
// **********************************************
//                   Forwards
// **********************************************

/**
 * Plugin is starting.
 *
 * @noreturn
 */
public _SurvExploitFixes_OnPluginStart()
{
	HookPublicEvent(EVENT_ONPLUGINENABLE, _SEF_OnPluginEnable);
	HookPublicEvent(EVENT_ONPLUGINDISABLE, _SEF_OnPluginDisable);
	
	g_iDebugChannel = DebugAddChannel(DEBUG_CHANNEL_NAME);	
	DebugPrintToAllEx("Module is now setup");
}

/**
 * Plugin is now enabled.
 *
 * @noreturn
 */
public _SEF_OnPluginEnable()
{
	// Smoker events
	HookEvent("choke_start", _SEF_ChokeStart_Event);
	HookEvent("tongue_grab", _SEF_TongueGrab_Event);
	HookEvent("tongue_release", _SEF_TongueRelease_Event);
	
	// Round events
	HookEvent("round_start", _SEF_RoundStart_Event);
	HookEvent("round_end", _SEF_RoundEnd_Event);
	
	// Player events
	HookEvent("heal_success", _SEF_HealSuccess_Event);
	HookEvent("player_death", _SEF_PlayerDeath_Event);	
	HookEvent("player_ledge_grab", _SEF_PlayerLedgeGrab_Event);		
	HookEvent("revive_begin", _SEF_ReviveBegin_Event);
	HookEvent("revive_success", _SEF_ReviveSuccess_Event);	
	HookEvent("player_bot_replace", _SEF_BotReplacePlayer_Event);
	HookEvent("bot_player_replace", _SEF_PlayerReplaceBot_Event);

	DebugPrintToAllEx( "Module is now loaded");	
}

/**
 * Plugin is now disabled.
 *
 * @noreturn
 */
public _SEF_OnPluginDisable()
{
	// Smoker events
	UnhookEvent("choke_start", _SEF_ChokeStart_Event);
	UnhookEvent("tongue_grab", _SEF_TongueGrab_Event);
	UnhookEvent("tongue_release", _SEF_TongueRelease_Event);
	
	// Round events
	UnhookEvent("round_start", _SEF_RoundStart_Event);
	UnhookEvent("round_end", _SEF_RoundEnd_Event);
	
	// Player events
	UnhookEvent("heal_success", _SEF_HealSuccess_Event);
	UnhookEvent("player_death", _SEF_PlayerDeath_Event);	
	UnhookEvent("player_ledge_grab", _SEF_PlayerLedgeGrab_Event);		
	UnhookEvent("revive_begin", _SEF_ReviveBegin_Event);
	UnhookEvent("revive_success", _SEF_ReviveSuccess_Event);
	UnhookEvent("player_bot_replace", _SEF_BotReplacePlayer_Event);
	UnhookEvent("bot_player_replace", _SEF_PlayerReplaceBot_Event);	
	
	DebugPrintToAllEx( "Module is now unloaded");
}


// Copy player states between bot - player and player - bot switches 
public _SEF_BotReplacePlayer_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	
	new player = GetClientOfUserId(GetEventInt(event, "player"));
	new bot = GetClientOfUserId(GetEventInt(event, "bot"));
	CopyPlayerState(player, bot);	
}

public _SEF_PlayerReplaceBot_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	
	new player = GetClientOfUserId(GetEventInt(event, "player"));
	new bot = GetClientOfUserId(GetEventInt(event, "bot"));
	CopyPlayerState(bot, player);	
}

public _SEF_TongueGrab_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new userid = GetEventInt(event, "victim");
	new client = GetClientOfUserId(userid);
	
	new attacker = GetEventInt(event, "attacker");
	
	// Start the monitor
	StartPulledHealthMonitor(client, attacker);
}

public _SEF_TongueRelease_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new userid = GetEventInt(event, "victim");
	new client = GetClientOfUserId(userid);
	
	// Mark the user as no longer being pulled
	playerState[client][PLAYER_PULLED] = 0;
	
	if (playerState[client][PLAYER_HANGING] == 1)
	{
		// Capture this to calculate additional damage done from hanging		
		CaptureHangingHealth(client);		
	}
	
	DebugPrintToAllEx( "Client %i: \"%N\" just released from pull", client, client);
}

public _SEF_ChokeStart_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new userid = GetEventInt(event, "victim");
	new client = GetClientOfUserId(userid);
	
	// Mark the user as no longer being pulled
	playerState[client][PLAYER_PULLED] = 0;
	
	DebugPrintToAllEx( "Client %i: \"%N\" is being choked", client, client);
}

public _SEF_PlayerLedgeGrab_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	// Determine which survivor is on ledge
	new userid = GetEventInt(event, "userid");
	new client = GetClientOfUserId(userid);
	
	// Mark the user as hanging
	playerState[client][PLAYER_HANGING] = 1;
	
	// Capture the current hanging health and temporary health
	CaptureHangingHealth(client);
	CaptureTemporaryHealth(client);		
}

public _SEF_HealSuccess_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new userid = GetEventInt(event, "subject");
	new client = GetClientOfUserId(userid);
	
	ResetPlayerState(client);
}

public _SEF_ReviveBegin_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new userid = GetEventInt(event, "subject");
	new client = GetClientOfUserId(userid);
	
	// Capture the current incap health
	playerState[client][HEALTH_REVIVE_START] = GetSurvivorPermanentHealth(client);
	DebugPrintToAllEx( "Client %i: \"%N\" revive started at %i", client, client, playerState[client][HEALTH_REVIVE_START]);
}

public _SEF_ReviveSuccess_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new userid = GetEventInt(event, "subject");
	new client = GetClientOfUserId(userid);
	StartSetHealthTimer(client);
	
	DebugPrintToAllEx( "Client %i: \"%N\" revive success", client, client);
}

public _SEF_RoundStart_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	DebugPrintToAllEx( "Survivor Exploit Fix Loaded");
}

public _SEF_PlayerDeath_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new userid = GetEventInt(event, "userid");
	new client = GetClientOfUserId(userid);
	
	if(client && GetClientTeam(client) != TEAM_INFECTED)
		ResetPlayerState(client);
}


public _SEF_RoundEnd_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	ResetAllPlayerStates();
}

/**
 * Starts the set health timer
 * 
 * @param timer			Handle to the timer object
 * @param client		Client object
 * @noreturn
 */
StartSetHealthTimer(client)
{	
	if (playerState[client][PLAYER_HANGING] == 1)
	{
		DebugPrintToAllEx("Starting revive timer for %i \"%N\"", client, client);
		playerState[client][Handle:TIMER_HEALTH_UPDATER] = CreateTimer(0.01, SetHealthTimer, client);
	}
}

/**
 * Called after revive success event if a player was hanging
 * 
 * @param timer			Handle to the timer object
 * @param client		Client object
 * @noreturn
 */
public Action:SetHealthTimer(Handle:timer, any:client)
{
	AdjustPermanentReviveHealth(client);	
	AdjustTemporaryReviveHealth(client);
	ResetPlayerState(client);
}

/**
 * Adjusts (if necessary) and Sets permanent health
 * Adjusts permanent health according to damage taken between tongue release and revive 
 * Scenario 1 - Player is smoked off the ledge
 * 		1a) Player was incapacitated
 * 				- Set health to pre-calculated health + 1
 * 				- Issue hurtme command for 1 damage (this is necessary for incaps - otherwise the player will glitch until player is hurt)
 * 		1b) Player was not incapacitated
 * 				- Adjust pre-calculated health to account for normal ledge damage
 * 				- Set health to pre-calculated health + 1
 * 				- Issue hurtme command for 1 damage (this is necessary for incaps - otherwise the player will glitch until player is hurt)
 * @param client		Client to set temporary health for
 * @noreturn
 */
AdjustPermanentReviveHealth(client)
{
	// Permanent health should be modified due to being pulled off a ledge
	if (playerState[client][HANDLE_REVIVE_HEALTH] == 1)
	{
		// Only modify revive health if player was not incapped
		if (playerState[client][PLAYER_WAS_INCAPPED] != 1)
		{
			// Modify calculated health based on amount of damage taken between tongue release and revive
			new ledgeReviveHealth = RoundToFloor(CalculateLedgeReviveHealth(client, float(playerState[client][HEALTH_SOLID_REVIVE])));
			playerState[client][HEALTH_SOLID_REVIVE] = ledgeReviveHealth;			
		}	
		SetEntityHealth(client, playerState[client][HEALTH_SOLID_REVIVE] + 1); // Note we are adding 1 to enable HurtMe to fire without effecting health
		HurtMe(client); // Must send this - otherwise health may be restored improperly if perm health drops to 0 with temp health
		
		DebugPrintToAllEx( "Set survivor health: %i", playerState[client][HEALTH_SOLID_REVIVE]); 
	}
	else
	{
		// Could handle temp health ledge hang issue
		// Players are not damaged until incap health drops below 300
		// Could modify health accordingly if HB is in effect
	}
	
}

/**
 * Adjusts (if necessary) and Sets temporary health
 * Adjusts temporary health according to damage taken between tongue release and revive 
 * Scenario 1 - Player fell off ledge with temporary health
 * 		1a) Permanent health is <= 1
 * 				- Set temp health with damage based on normal ledge damage rates with normal decay
 * 		1b) Permanent health is > 1
 * 				- Set original health bonus with normal decay
 * Scenario 2 - Player was incapacitated while on ledge
 * 		2a) Player Didn't die from incap
 * 				- Add 1 to incap count
 * 				- Set temp health to 31
 * 				- Issue hurt me command for 1 damage (this is necessary - otherwise the player will glitch until player is hurt)
 * 		2b) Player died from incap (NOT HANDLED HERE)
 * 				- Is handled in PulledHealthMonitor
 * @param client		Client to set temporary health for
 * @noreturn
 */
AdjustTemporaryReviveHealth(client)
{
	// Temporary health should be modified due to ledge hange (could be with or without a smoker)	
	if (playerState[client][HANDLE_REVIVE_TEMP_HEALTH] == 1)
	{
		// Modifies temporary health based on time between tongue release and revive
		if (playerState[client][PLAYER_WAS_INCAPPED] == 0)
		{
			DebugPrintToAllEx( "Doing temp health without incap - %i", GetClientHealth(client));
			
			// Additional damage to temp health (only if solid health <= 1)
			if (GetClientHealth(client) <= 1)
			{
				playerState[client][Float:HEALTH_TEMP_REVIVE] = CalculateLedgeReviveHealth(client, playerState[client][Float:HEALTH_TEMP_REVIVE]);
				SetSurvivorTempHealth(client, playerState[client][Float:HEALTH_TEMP_REVIVE], playerState[client][Float:HEALTH_BUFFER_TIME]);
				DebugPrintToAllEx( "Updated HB: %f at %f", playerState[client][Float:HEALTH_TEMP_REVIVE], playerState[client][Float:HEALTH_BUFFER_TIME]);
			}
			else // Don't damage temp health further, just restore for decay rate
			{
				SetSurvivorTempHealth(client, playerState[client][Float:HEALTH_BUFFER], playerState[client][Float:HEALTH_BUFFER_TIME]);
				DebugPrintToAllEx( "Reset HB: %f at %f", playerState[client][Float:HEALTH_BUFFER], playerState[client][Float:HEALTH_BUFFER_TIME]);			
			}
		}
		else // Tick incap count and set revive health
		{
			new currentIncapCount = GetSurvivorIncapCount(client);
			currentIncapCount++;
			SetEntProp(client, Prop_Send, "m_currentReviveCount", currentIncapCount);
			
			SetSurvivorTempHealth(client, playerState[client][Float:HEALTH_TEMP_REVIVE], 0.0);		
			HurtMe(client);
			
			DebugPrintToAllEx( "Incap count: %i | Incapped Health Bonus: %f", currentIncapCount, playerState[client][Float:HEALTH_TEMP_REVIVE]);
		}
	}	
}

/**
 * Calculates damage done to health from hanging
 * Used to adjust revive health calculated from a ledge pull
 *
 * @param client		Client to use for calculations
 * @return 				Modified health value based on hanging health and starting revive health
 */
Float:CalculateLedgeReviveHealth(client, Float:health)
{
	// Modify calculated health based on amount of damage taken between tongue release and revive
	new initialLedgeHealth = playerState[client][HEALTH_HANGING_OR_RELEASE]; 	// Health on tongue release
	new reviveLedgeHealth = playerState[client][HEALTH_REVIVE_START];			// Health on revive start
	
	// Calculate percentage of health that should remain
	new Float:dmgPercent = float(reviveLedgeHealth) / float(initialLedgeHealth);	
	new Float:preciseHealth = health * dmgPercent;

	DebugPrintToAllEx( "ILH: %i, RLH: %i, CH: %f DP: %f, NH: %f", initialLedgeHealth, reviveLedgeHealth, health, dmgPercent, preciseHealth);
	
	return preciseHealth;
}

/**
 * Called on tongue drag event to monitor player's health (before ledge fall)
 *
 * @param client		Client whose health should be monitored
 * @noreturn
 */
StartPulledHealthMonitor(client, attackerId)
{
	ResetPlayerState(client);
	
	playerState[client][PLAYER_PULLED_BY] = attackerId;
	playerState[client][PLAYER_PULLED] = 1;	
	
	CapturePermanentHealth(client);		
	playerState[client][Handle:TIMER_HEALTH_TRACKER] = CreateTimer(0.05, PulledHealthMonitor, client, TIMER_FLAG_NO_MAPCHANGE|TIMER_REPEAT);
	
	DebugPrintToAllEx( "Health monitor started for \"%N\"", client);
}

/**
 * Monitors health while a survivor is being pulled
 * Stops when a survivor is incapacitated, dies or is choked
 *
 * @param timer			Handle to the timer object.
 * @param client		Client index of pulled player
 * @noreturn
 */
public Action:PulledHealthMonitor(Handle:timer, any:client)
{
	new bool:isPulled = playerState[client][PLAYER_PULLED] == 1;
	new bool:isIncapped = playerState[client][PLAYER_WAS_INCAPPED] == 1;
	new bool:isHanging = playerState[client][PLAYER_HANGING] == 1;
	
	if (!isIncapped && isPulled) // Stop on incap or when pull is over
	{
		if (!isHanging) // Capture permanent health prior to ledge
		{
			CapturePermanentHealth(client);
			DebugPrintToAllEx( "Logging player health: %i", playerState[client][HEALTH_SOLID]);
		}
		else // Calculate health while being pulled on a ledge
		{
			// Handle health on revive
			playerState[client][HANDLE_REVIVE_HEALTH] = 1;
			
			new Float:currentHealth = CalculateSmokedHangHealth(client);
			
			if (currentHealth < 1.0) // Player should be incapacitated or dead
			{
				IncapacitateSurvivor(client);
				playerState[client][Handle:TIMER_HEALTH_TRACKER] = INVALID_HANDLE;
				return Plugin_Stop;
			}
		}		
		return Plugin_Continue;
	}
	else
	{
		playerState[client][Handle:TIMER_HEALTH_TRACKER] = INVALID_HANDLE;
		return Plugin_Stop;
	}
}

/**
 * Calculates remaining health of survivor being pulled while hanging on a ledge
 * Used by the PulledHealthMonitor
 *
 * @param client		Client index of pulled player
 * @return				Total health remaining
 */
Float:CalculateSmokedHangHealth(client)
{	
	// Determine total damage done by smoker so far
	new Float:startingHangingHealth = float(playerState[client][HEALTH_HANGING_OR_RELEASE]);
	new Float:currentHangingHealth = float(GetSurvivorPermanentHealth(client));	
	new Float:totalDamage = (startingHangingHealth - currentHangingHealth) / SMOKER_CLAW_DAMAGE;
	
	// Estimate current solid health using solid health remaining right before ledge grab	
	new Float:solidHealth = float(playerState[client][HEALTH_SOLID]);	
	new Float:newSolidHealth = solidHealth - totalDamage;
	
	// Estimate current temp health
	// Only damage temp health if damage exceeds solid health
	new Float:tempHealth = GetSurvivorTempHealth(client);
	new Float:tempHealthDmg = totalDamage - solidHealth;
	new Float:newTempHealth = tempHealth - (tempHealthDmg > 0.0 ? tempHealthDmg : 0.0);
	
	// Capture calculated health
	playerState[client][HEALTH_SOLID_REVIVE] = newSolidHealth < 0.0 ? 0 : RoundToFloor(newSolidHealth);
	playerState[client][Float:HEALTH_TEMP_REVIVE] = newTempHealth < 0.0 ? 0.0 : newTempHealth;
	
	DebugPrintToAllEx( "TD: %f | TPH: %i | TTH %f | TH: %f", totalDamage, playerState[client][HEALTH_SOLID_REVIVE], playerState[client][Float:HEALTH_TEMP_REVIVE], float(playerState[client][HEALTH_SOLID_REVIVE]) + playerState[client][Float:HEALTH_TEMP_REVIVE]);
	
	// Return total remaining health
	return float(playerState[client][HEALTH_SOLID_REVIVE]) + playerState[client][Float:HEALTH_TEMP_REVIVE];
}

/**
 * Determines what to do when a player gets incapacitated by a smoker while on a ledge
 * Scenario 1 - Incap count < 2
 * 		-	Set flags to enable plugin to adjust health on revive
 * 		-	Send message to chat to let everyone know player was incapacitated
 * 
 * Scenario 2 - Incap count >= 2
 * 		-	Set flags to release player from ledge and make them fall
 * 		-	Adjust health buffer to 0.0 (fixes remaining buffer incase game thinks they have more)
 * 		-	Kill player
 * 		-	Send message to chat to let everyone know player has died
 * 
 * @param client		Client index of player
 */
IncapacitateSurvivor(client)
{	
	// Get incap count and determine if action needs to be taken
	new currentIncapCount = GetSurvivorIncapCount(client);
	if (currentIncapCount < 2)
	{
		currentIncapCount++;		
		playerState[client][HANDLE_REVIVE_HEALTH] = 1;
		playerState[client][HANDLE_REVIVE_TEMP_HEALTH] = 1;
		playerState[client][HEALTH_SOLID_REVIVE] = 0;
		playerState[client][Float:HEALTH_TEMP_REVIVE] = 31.0;
		playerState[client][PLAYER_WAS_INCAPPED] = 1;
		
		new attacker = GetClientOfUserId(playerState[client][PLAYER_PULLED_BY]);
		if (attacker)
			PrintToChatAll("[rotoblin] %N incapacitated %N", attacker, client);
		else
			PrintToChatAll("[rotoblin] %s incapacitated %N", "Smoker", client);
	}
	else
	{
		DebugPrintToAllEx( "Player %i \"%N\" should be dead", client, client);
		SetEntProp(client, Prop_Send, "m_isIncapacitated", 0);
		SetEntProp(client, Prop_Send, "m_isHangingFromLedge", 0);
		SetEntProp(client, Prop_Send, "m_isFallingFromLedge", 1);
		SetEntPropFloat(client, Prop_Send, "m_healthBuffer", 0.0);
		ForcePlayerSuicide(client);
		
		new attacker = GetClientOfUserId(playerState[client][PLAYER_PULLED_BY]);
		if (attacker)
			PrintToChatAll("[rotoblin] %N killed %N", attacker, client);
		else
			PrintToChatAll("[rotoblin] %s killed %N", "Smoker", client);		
	}
}

/* Temporary health functions */
Float:GetSurvivorTempHealth(client)
{
	new Float:temphp = GetEntPropFloat(client, Prop_Send, "m_healthBuffer") - ((GetGameTime() - GetEntPropFloat(client, Prop_Send, "m_healthBufferTime")) * GetConVarFloat(FindConVar("pain_pills_decay_rate"))) - 1.0;
	return Float:temphp > 0.0 ? temphp : 0.0;
}

/**
 * Sets a clients temporary health
 *
 * @param client			Client index of player to snapshot
 * @param Float:health		Amount of temporary health
 * @param Float:startTime	Starting decay time (0.0 defaults to current game time)
 */
SetSurvivorTempHealth(client, Float:health, Float:startTime)
{
	if (startTime <= 0.0)
		startTime = GetGameTime();
	
	SetEntPropFloat(client, Prop_Send, "m_healthBuffer", health);
	SetEntPropFloat(client, Prop_Send, "m_healthBufferTime", startTime);
}

/**
 * Takes a snapshot of a clients temporary health
 *
 * @param client		Client index of player to snapshot
 */
CaptureTemporaryHealth(client)
{
	// Capture health buffer
	playerState[client][Float:HEALTH_TEMP_REVIVE] = GetSurvivorTempHealth(client);
	playerState[client][Float:HEALTH_BUFFER] = GetEntPropFloat(client, Prop_Send, "m_healthBuffer");
	playerState[client][Float:HEALTH_BUFFER_TIME] = GetEntPropFloat(client, Prop_Send, "m_healthBufferTime");
	DebugPrintToAllEx("Stored temporary health: TH: %f HB: %f | HBT: %f", playerState[client][Float:HEALTH_TEMP_REVIVE], playerState[client][Float:HEALTH_BUFFER], playerState[client][Float:HEALTH_BUFFER_TIME]);
}


/**
 * Gets permanent health
 *
 * @param client		Client index of player
 */
GetSurvivorPermanentHealth(client)
{
	return GetEntProp(client, Prop_Send, "m_iHealth");
}

/**
 * Sets permanent health
 *
 * @param client		Client index of player
 */
SetSurvivorPermanentHealth(client, health)
{
	health = health > 0 ? health : 0;
	SetEntProp(client, Prop_Send, "m_iHealth", health);
}

/**
 * Takes a snapshot of a clients permanent health
 *
 * @param client		Client index of player to snapshot
 */
CapturePermanentHealth(client)
{
	playerState[client][HEALTH_SOLID] = GetSurvivorPermanentHealth(client);
	return playerState[client][HEALTH_SOLID];
}

/**
 * Takes a snapshot of a clients hanging healh
 *
 * @param client		Client index of player to snapshot
 */
CaptureHangingHealth(client)
{
	playerState[client][HEALTH_HANGING_OR_RELEASE] = GetSurvivorPermanentHealth(client);
	DebugPrintToAllEx("Storing hanging health: %i", playerState[client][HEALTH_HANGING_OR_RELEASE]);
}

/**
 * Gets a survivors incap count
 *
 * @param client		Client index of player
 */
GetSurvivorIncapCount(client)
{
	return GetEntProp(client, Prop_Send, "m_currentReviveCount");
}

/**
 * Resets player state tracked by the plugin
 *
 * @param client		Client index of player to reset
 * @noreturn
 */
ResetPlayerState(client)
{		
	playerState[client][PLAYER_PULLED] = 0;
	playerState[client][PLAYER_HANGING] = 0;
	playerState[client][PLAYER_WAS_INCAPPED] = 0;
	
	playerState[client][HEALTH_SOLID] = 0;
	playerState[client][HEALTH_HANGING_OR_RELEASE] = 0;
	playerState[client][HEALTH_REVIVE_START] = 0;
	playerState[client][HEALTH_SOLID_REVIVE] = -1;
	playerState[client][Float:HEALTH_TEMP_REVIVE] = -1.0;
	
	playerState[client][HANDLE_REVIVE_HEALTH] = 0;
	playerState[client][HANDLE_REVIVE_TEMP_HEALTH] = 1; // Set to 1 to always handle temp health on revive
	
	playerState[client][Float:HEALTH_BUFFER] = 0.0;
	playerState[client][Float:HEALTH_BUFFER_TIME] = 0.0;
	
	DebugPrintToAllEx("Reset player state for client %i \"%N\"", client, client);
}

/**
 * Resets all player states tracked by the plugin
 *
 * @noreturn
 */
ResetAllPlayerStates()
{
	for(new i = 0; i < MAXPLAYERS+1; i++)
	{
		ResetPlayerState(i);
	}
}

/**
 * Copies player state from one player to another
 *
 * @param clientFrom		Client to copy from
 * @param clientTo			Client to copy to
 * @noreturn
 */
CopyPlayerState(clientFrom, clientTo)
{
	playerState[clientTo][PLAYER_PULLED] = playerState[clientFrom][PLAYER_PULLED];
	playerState[clientTo][PLAYER_PULLED_BY] = playerState[clientFrom][PLAYER_PULLED_BY];
	playerState[clientTo][PLAYER_HANGING] = playerState[clientFrom][PLAYER_HANGING];
	playerState[clientTo][PLAYER_WAS_INCAPPED] = playerState[clientFrom][PLAYER_WAS_INCAPPED];	
	
	playerState[clientTo][HEALTH_SOLID] = playerState[clientFrom][HEALTH_SOLID];
	playerState[clientTo][HEALTH_HANGING_OR_RELEASE] = playerState[clientFrom][HEALTH_HANGING_OR_RELEASE];
	playerState[clientTo][HEALTH_REVIVE_START] = playerState[clientFrom][HEALTH_REVIVE_START];
	playerState[clientTo][HEALTH_SOLID_REVIVE] = playerState[clientFrom][HEALTH_SOLID_REVIVE];
	playerState[clientTo][Float:HEALTH_TEMP_REVIVE] = playerState[clientFrom][Float:HEALTH_TEMP_REVIVE];
	
	playerState[clientTo][HANDLE_REVIVE_HEALTH] = playerState[clientFrom][HANDLE_REVIVE_HEALTH];
	playerState[clientTo][HANDLE_REVIVE_TEMP_HEALTH] = playerState[clientFrom][HANDLE_REVIVE_TEMP_HEALTH];
	
	playerState[clientTo][Float:HEALTH_BUFFER] = playerState[clientFrom][Float:HEALTH_BUFFER];
	playerState[clientTo][Float:HEALTH_BUFFER_TIME] = playerState[clientFrom][Float:HEALTH_BUFFER_TIME];
	
	DebugPrintToAllEx("Copied player state to client %i from client %i", clientFrom, clientTo);
	// Restart Timers
	if (playerState[clientFrom][PLAYER_PULLED] == 1 && playerState[clientFrom][Handle:TIMER_HEALTH_TRACKER] != INVALID_HANDLE)
	{
		CloseHandle(playerState[clientFrom][Handle:TIMER_HEALTH_TRACKER]);
		playerState[clientFrom][Handle:TIMER_HEALTH_TRACKER] = INVALID_HANDLE;
		
		StartPulledHealthMonitor(clientTo, playerState[clientTo][PLAYER_PULLED_BY]);
	}
	
	ResetPlayerState(clientFrom);
}

/**
 * Hurt client for 1 damage
 * Used to fix incaps on revive
 *
 * @param clientFrom		Client to copy from
 * @param clientTo			Client to copy to
 * @param client			Client index of player
 * @noreturn
 */
HurtMe(client)
{
	new String:command[] = "hurtme";
	new flags = GetCommandFlags(command);
	
	SetCommandFlags(command, flags & ~FCVAR_CHEAT);
	
	FakeClientCommand(client, "hurtme 1");
	
	// restore z_spawn
	SetCommandFlags(command, flags);
}

/**
 * Wrapper for printing a debug message without having to define channel index
 * everytime.
 *
 * @param format		Formatting rules.
 * @param ...			Variable number of format parameters.
 * @noreturn
 */
static DebugPrintToAllEx(const String:format[], any:...)
{
	decl String:buffer[DEBUG_MESSAGE_LENGTH];
	VFormat(buffer, sizeof(buffer), format, 2);
	DebugPrintToAll(g_iDebugChannel, buffer);
}